{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The Signature Method with Sktime\n",
    "\n",
    "The 'signature method' refers to a collection of feature extraction techniques for multimodal sequential data, derived from the theory of controlled differential equations. In recent years, a large number of modifications have been suggested to the signature method so as to improve some aspect of it. \n",
    "\n",
    "In the paper [\"A Generalised Signature Method for Time-Series\"](https://arxiv.org/abs/2006.00873) [1] the authors collated the vast majority of these modifications into a single document and ran a large hyper-parameter study over the multivariate UEA datasets to build a generic signature algorithm that is expected to work well on a wide range of datasets. We implement the best practice results from this study as the default starting values for our hyperparameters in the `SignatureClassifier` module. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The Path Signature\n",
    "At the heart of the signature method is the so-called \"signature transform\".\n",
    "\n",
    "A path $X$ of finite length in $\\textit{d}$ dimensions can be described by the mapping $X:[a, b]\\rightarrow\\mathbb{R}$ $\\!\\!^d$, or in terms of coordinates $X=(X^1_t, X^2_t, ...,X^d_t)$, where each coordinate $X^i_t$ is real-valued and parameterised by $t\\in[a,b]$.\n",
    "\n",
    "The **signature transform** $S$ of a path $X$ is defined as an infinite sequence of values:\n",
    "\\begin{equation} \n",
    "    S(X)_{a, b} = (1, S(X)_{a, b}^1, S(X)_{a, b}^2, ..., S(X)_{a, b}^d, S(X)_{a,b}^{1, 1}, S(X)_{a,b}^{1, 2}, ...),\n",
    "    \\label{eq:path_signature}\n",
    "\\end{equation}\n",
    "where each term is a $k$-fold iterated integral of $X$ with multi-index $i_1,...,i_k$:\n",
    "\\begin{equation}\n",
    "    S(X)_{a, b}^{i_1,...,i_k} = \\int_{a<t_k<b}...\\int_{a<t_1<t_2} \\mathrm{d}X_{t_1}^{i_1}...\\mathrm{d}X_{t_k}^{i_k}.\n",
    "    \\label{eq:sig_moments}\n",
    "\\end{equation}\n",
    "This defines a graded sequence of numbers associated with a path which is known to characterise it up to a generalised form of reparameterisation [2]. One can think of the signature as a collection of summary statistics that determine a path (almost) uniquely. Furthermore, any continuous function on the path $X$ can be approximated arbitrarily well as a linear function on its signature [3]; the signature unravels the non-linearities on functions on the space of unparameterised paths. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### A Visualisation\n",
    "To give an idea of what the signature terms represent physically, we consider a patient in an ICU where we are tracking their systolic blood pressure (SBP) and heart rate (HR) changing in time. This can be represented as a path in $\\mathbb{R}^3$ (assuming time is included as a channel).\n",
    "\n",
    "![signature_visualisation](img/signatures_term_visualisation.png)\n",
    "\n",
    "The plot above sketches two scenarios of how such a path might look. We are assuming here an implicit time dimension for each plot such that the path is traversed from left to right along the blue line. \n",
    "\n",
    "#### Depth 1:\n",
    "The signature terms to depth 1 are simply the changes of each of the variables over the interval, in the image this is the $\\Delta \\text{HR}$ and $\\Delta \\text{SBP}$ terms. Note that these values are the same in each case.\n",
    "\n",
    "#### Depth 2: \n",
    "The second level gives us the signed areas (the shaded orange regions), where the orientation of the left most plot is such that the negatively signed area is produced whereas the second gives the positive value, and thus, at order 2 in the signature we now have sufficient information to discriminate between these two situations where in the first rise in heart rate occurs before (or at least, initially faster than) the rise in blood pressure, and vice versa.\n",
    "\n",
    "\n",
    "#### Depth > 2: \n",
    "Depths larger than 2 become more difficult to visualise graphically, however the idea is similar to that of the depth 2 case where we saw that the signature produced information on whether the increase in HR or SBP appeared to be happening first, along with some numerical quantification of how much this was happening. At higher orders the signature is doing something similar, but now with three events, rather than two. The signature picks out structural information regarding the order in which events occur. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### The Signature in Time-Series Analysis\n",
    "The signature is a natural tool to apply in problems related to time-series analysis. As described above it can convert multi-dimensional time-series data into static features that represent information about the sequential nature of the time-series, that can be fed through a standard machine learning model. \n",
    "\n",
    "A simplistic view of how this works is as follows:\n",
    "\\begin{equation}\n",
    "    \\text{Model}(\\text{Signature}(\\text{Sequential data}))) = \\text{Predictions}\n",
    "\\end{equation}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Considered Signature Variations\n",
    "Again, following the work in [1] we group the variations on the signature method conceptually into:\n",
    "\n",
    "- **Augmentations** - Transformation of an input sequence or time series into one or more new sequences, so that the signature will return different information about the path.\n",
    "- **Windows** - Windowing operations, so that the signature can act with some locality.\n",
    "- **Transforms** - The choice between the signature or the logsignature transformation.\n",
    "- **Rescalings** - Method of signature rescaling.\n",
    "\n",
    "This is neatly represented in the following graphic, where $\\phi$ represents the augmentation, $W^{i, j}$ the windowing operation, $S^N$ the signature, and $\\rho_{\\text{pre}/\\text{post}}$ the rescalig method. \n",
    "\n",
    "<img class=\"center\" src=\"img/signatures_generalised_method.png\" width=\"500\"/>\n",
    "\n",
    "\n",
    "Please refer to the full paper for a more comprehensive exploration into what each of these groupings means. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The Sktime Modules\n",
    "We now give an introduction to the classification and transformation modules included in th sktime interface, along with an example to show how to perform efficient hyperparameter optimisation that was found to give good results in [1]. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true,
    "execution": {
     "iopub.execute_input": "2021-06-25T12:41:09.835788Z",
     "iopub.status.busy": "2021-06-25T12:41:09.834965Z",
     "iopub.status.idle": "2021-06-25T12:41:11.521536Z",
     "shell.execute_reply": "2021-06-25T12:41:11.522026Z"
    },
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "outputs": [],
   "source": [
    "# Some additional imports we will use\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "from sktime.datasets import load_gunpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-06-25T12:41:11.529792Z",
     "iopub.status.busy": "2021-06-25T12:41:11.529157Z",
     "iopub.status.idle": "2021-06-25T12:41:11.609172Z",
     "shell.execute_reply": "2021-06-25T12:41:11.609562Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "D:\\CMP Machine Learning\\sktime-workshop-boss\\sktime\\utils\\data_io.py:63: FutureWarning: This function has moved to datasets/_data_io, this version will be removed in V0.10\n",
      "  warn(\n"
     ]
    }
   ],
   "source": [
    "# Load an example dataset\n",
    "train_x, train_y = load_gunpoint(split=\"train\", return_X_y=True)\n",
    "test_x, test_y = load_gunpoint(split=\"test\", return_X_y=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Overview\n",
    "We provide the following:\n",
    "- **sktime.transformers.panel.signature_based.SignatureTransformer** - An sklearn transformer that provides the functionality to apply the signature method with some choice of variations as noted above.\n",
    "- **sktime.classification.feature_based.SignatureClassifier** - This provides a simple interface to append a classifier to the SignatureTransformer class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true,
    "execution": {
     "iopub.execute_input": "2021-06-25T12:41:11.615463Z",
     "iopub.status.busy": "2021-06-25T12:41:11.614397Z",
     "iopub.status.idle": "2021-06-25T12:41:11.714204Z",
     "shell.execute_reply": "2021-06-25T12:41:11.714709Z"
    },
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "outputs": [],
   "source": [
    "from sktime.classification.feature_based import SignatureClassifier\n",
    "from sktime.transformations.panel.signature_based import SignatureTransformer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Example 1: Sequential Data -> Signature Features.\n",
    "Here we will give a very simple example of converting the sequential 3D GunPoint data of shape [num_batch, series_length, num_features] -> [num_batch, signature_features]."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-06-25T12:41:11.720089Z",
     "iopub.status.busy": "2021-06-25T12:41:11.719386Z",
     "iopub.status.idle": "2021-06-25T12:41:11.751292Z",
     "shell.execute_reply": "2021-06-25T12:41:11.751715Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Raw data shape is: (50, 1)\n",
      "Signature shape is: (50, 14)\n"
     ]
    }
   ],
   "source": [
    "# First build a very simple signature transform module\n",
    "signature_transform = SignatureTransformer(\n",
    "    augmentation_list=(\"addtime\",),\n",
    "    window_name=\"global\",\n",
    "    window_depth=None,\n",
    "    window_length=None,\n",
    "    window_step=None,\n",
    "    rescaling=None,\n",
    "    sig_tfm=\"signature\",\n",
    "    depth=3,\n",
    ")\n",
    "\n",
    "# The simply transform the stream data\n",
    "print(f\"Raw data shape is: {train_x.shape}\")\n",
    "train_signature_x = signature_transform.fit_transform(train_x)\n",
    "print(f\"Signature shape is: {train_signature_x.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It then becomes extremely easy to build a time-series classification model. For example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-06-25T12:41:11.791725Z",
     "iopub.status.busy": "2021-06-25T12:41:11.757236Z",
     "iopub.status.idle": "2021-06-25T12:41:11.945002Z",
     "shell.execute_reply": "2021-06-25T12:41:11.945539Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Accuracy: 0.740%\n"
     ]
    }
   ],
   "source": [
    "# Train\n",
    "model = RandomForestClassifier()\n",
    "model.fit(train_signature_x, train_y)\n",
    "\n",
    "# Evaluate\n",
    "test_signature_x = signature_transform.transform(test_x)\n",
    "test_pred = model.predict(test_signature_x)\n",
    "print(f\"Accuracy: {accuracy_score(test_y, test_pred):.3f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Example 2: Fine Tuning the Generalised Model\n",
    "As previously mentioned, in [1] the authors performed a large hyperparameter search over the signature variations on the full UEA archive to develop a 'Best Practices' approach to building a model. This required some fine tuning over the following parameters, as they were found to be very dataset specific: \n",
    "- `depth` over [1, 2, 3, 4, 5, 6]\n",
    "- `window_depth` over [2, 3, 4]\n",
    "- `RandomForestClassifier` hyperparameters.\n",
    "\n",
    "Here we show how this is easily done using the sktime framework. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true,
    "execution": {
     "iopub.execute_input": "2021-06-25T12:41:12.028987Z",
     "iopub.status.busy": "2021-06-25T12:41:11.953302Z",
     "iopub.status.idle": "2021-06-25T12:41:46.774230Z",
     "shell.execute_reply": "2021-06-25T12:41:46.774764Z"
    },
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Train acc: 100.000%  |  Test acc: 96.000%\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import RandomizedSearchCV, StratifiedKFold\n",
    "\n",
    "# Some params\n",
    "n_cv_splits = 5\n",
    "n_gs_iter = 20\n",
    "\n",
    "# Random forests found to perform very well in general\n",
    "estimator = RandomForestClassifier()\n",
    "\n",
    "# The grid to be passed to an sklearn gridsearch\n",
    "signature_grid = {\n",
    "    # Signature params\n",
    "    \"depth\": [1, 2, 3, 4, 5],\n",
    "    \"window_name\": [\"dyadic\"],\n",
    "    \"augmentation_list\": [[\"basepoint\", \"addtime\"]],\n",
    "    \"window_depth\": [1, 2, 3, 4],\n",
    "    \"rescaling\": [\"post\"],\n",
    "    # Classifier and classifier params\n",
    "    \"estimator\": [estimator],\n",
    "    \"estimator__n_estimators\": [50, 100, 500],\n",
    "    \"estimator__max_depth\": [2, 4, 6, 8, 12, 16, 24, 32, 45, 60],\n",
    "}\n",
    "\n",
    "# Initialise the estimator\n",
    "estimator = SignatureClassifier()\n",
    "\n",
    "# Run a random grid search and return the gs object\n",
    "cv = StratifiedKFold(n_splits=n_cv_splits)\n",
    "gs = RandomizedSearchCV(estimator, signature_grid, cv=n_cv_splits, n_iter=n_gs_iter)\n",
    "gs.fit(train_x, train_y)\n",
    "\n",
    "# Get the best classifier\n",
    "best_classifier = gs.best_estimator_\n",
    "\n",
    "# Evaluate\n",
    "train_preds = best_classifier.predict(train_x)\n",
    "test_preds = best_classifier.predict(test_x)\n",
    "train_score = accuracy_score(train_y, train_preds)\n",
    "test_score = accuracy_score(test_y, test_preds)\n",
    "print(f\"Train acc: {train_score * 100:.3f}%  |  Test acc: {test_score * 100:.3f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## A Full Description of the Parameters\n",
    "We conclude by giving further explanation of each of the parameters in the `SignatureClassifier` module and what values they can take. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Parameters\n",
    "----------\n",
    "Below we list each parameter and the values that they can take. For further details about what the options mean refer to [1].\n",
    "\n",
    "\n",
    "**classifier** Needs to be any sklearn estimator. Defaults to `RandomForestClassifier()`.\n",
    "\n",
    "**augmentation_list**: list of tuple of strings, List of augmentations to be applied before the signature transform is applied. These can be any from:\n",
    "- 'addtime' - Add an equally spaced time channel.\n",
    "- 'leadlag' - The leadlag transform.\n",
    "- 'ir' - Perform the invisibility reset transform.\n",
    "- 'cumsum' - Perform a cumulative sum transform.\n",
    "- 'basepoint' - Append zero to the start of the path to remove translational invariance.\n",
    "\n",
    "**window_name** str, The name of the window transform to apply. Can be any of:\n",
    "- 'global' - A single window over all the data.\n",
    "- 'expanding' - Multiple windows starting at the first datapoint that extend over the data (increasing width).\n",
    "- 'sliding' - Multiple windows that slide along the data (fixed width).\n",
    "- 'dyadic' - Partition the data into dyadic windows.\n",
    "\n",
    "**window_depth**: int, The depth of the dyadic window. (Active only if `window_name == 'dyadic']`).\n",
    "\n",
    "**window_length**: int, The length of the sliding/expanding window. (Active only if `window_name in ['sliding, 'expanding'].`)\n",
    "\n",
    "**window_step**: int, The step of the sliding/expanding window. (Active only if `window_name in ['sliding, 'expanding'].`)\n",
    "\n",
    "**rescaling**: str, The method of signature rescaling. Any of:\n",
    "- 'pre' - Rescale the path before the signature transform.\n",
    "- 'post' - Rescale the path after the signature transform.\n",
    "- None - No rescaling.\n",
    "\n",
    "**sig_tfm**: str, String to specify the type of signature transform. Either of: ['signature', 'logsignature'].\n",
    "\n",
    "**depth**: int, Signature truncation depth.\n",
    "\n",
    "**random_state**: int, Random state initialisation."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## References\n",
    "[1] Morrill, James, Adeline Fermanian, Patrick Kidger, and Terry Lyons. \"A Generalised Signature Method for Time Series.\" arXiv preprint arXiv:2006.00873 (2020).\n",
    "\n",
    "[2] Hambly, B., Lyons, T.: Uniqueness for the signature of a path of bounded variation and the reduced pathgroup. Annals of Mathematics171(1), 109–167 (2010). doi:10.4007/annals.2010.171.10913.   \n",
    "\n",
    "[3] Lyons, T.J.: Differential equations driven by rough signals. Revista Matem ́atica Iberoamericana14(2), 215–310(1998)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
